/*
 * CSIStyleAggregator.java
 * 
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 * 
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 * 
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */

package vegadataeditor.output.dataAggregators;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import vegadataeditor.VegaStreamContainer;
import org.apache.commons.math.stat.StatUtils;

/**
 * This mimicks the style of aggregation done by Campbell Scientific
 * data loggers. This is important because a lot of legacy data is campbell
 * data logger aggregated measurements.
 *
 * @author lawinslow
 */
public class CSIStyleAggregator implements ITimeModifier {
    public static enum aggTypes{
        MEAN,
        SUM,
        MAX,
        MIN,
        STDDEV
    }

    /** Type of aggregation to do. Default is MEAN. */
    private aggTypes selectedAggType = aggTypes.MEAN;

    /** What length time window is used for the aggregation applied.
     * Default is 1 hour. Stored in mills. */
    private long aggSpan = 60*60*1000;



    /**
     * Constructor using defaults of 1 hour Mean aggregation.
     */
    public CSIStyleAggregator(){
        this.aggSpan = 60*60*1000;//default to 1hour
        this.selectedAggType = aggTypes.MEAN;
    }

    /**
     * Constructor which immediately sets up aggregation type and span
     * @param aggType Aggregation type from aggTypes enum.
     * @param span Aggregation Span in milliseconds.
     */
    public CSIStyleAggregator(aggTypes aggType,long span){
        this();
        
        this.aggSpan = span;
        this.selectedAggType = aggType;
    }

    /**
     * Function that modifies the dates onto the new timeseries
     * given based on the method set by <code>setAggType</code> method. A new
     * timeseries is generated by estimating the start  and end of the
     * timeseries and using the aggspan supplied.
     *
     * @param v The dataset to be aggregated
     * @return The aggregated dataset in a ModifierResult object
     */
    public ModifierResult modify(VegaStreamContainer v) {
        return modify(v,this.createTimes(v.datetime));
    }

    /**
     * Function that modifies the dates onto the new timeseries
     * given based on the method set by <code>setAggType</code> method. In this
     * modify method, aggspan is ignored and the parameter newTimes is used.
     * @param v The dataset to be aggregated
     * @param newTimes The list of times to aggregate onto
     * @return The aggregated dataset in a ModifierResult object
     */
    public ModifierResult modify(VegaStreamContainer v, List<Date> newTimes){
        if(newTimes.size()<2){
            throw new IllegalArgumentException("New timestamp list must have" +
                    " at least two timestamps.");
        }
        //TreeMap<Date,Double> outputTS = new TreeMap<Date,Double>();
        ArrayList<Date> outputTimes = new ArrayList<Date>();
        ArrayList<Double> values = new ArrayList<Double>();
        ArrayList<Integer> poolCounts = new ArrayList<Integer>();
        TreeMap<Date,Double> timeseries = new TreeMap<Date,Double>();

        for(int i=0;i<v.datetime.size();i++){
            timeseries.put(v.datetime.get(i), v.values.get(i));
        }

        Date low;
        Date high;
        SortedMap tmpMap;

        for(int i=0;i<newTimes.size()-2;i++){
            low = newTimes.get(i);
            high = newTimes.get(i+1);
            //The low end should be simply 'greater than' so add one millisec
            // so it doesn't equal the lower value.
            tmpMap = timeseries.subMap(new Date(low.getTime()+1), new Date(high.getTime()+1));
            //if there are no values in this range, skip.
            if(tmpMap.size()<1){
                continue;
            }
            outputTimes.add(high);
            values.add(calculateAggregate(tmpMap));
            poolCounts.add(tmpMap.size());
        }
        
        ModifierResult r = new ModifierResult();
        r.setTimes(outputTimes);
        r.setValues(values);
        r.setAggPoolNumber(poolCounts);
        return r;
    }

    /**
     * Builds a list of new times that the supplied data to which the data
     * will be aggregated. List is in ascending order.
     *
     * @param times Original data timestamps.
     * @return New timestamps to aggregate to.
     */
    private List<Date> createTimes(List<Date> times){
        Date end = (Date)java.util.Collections.max(times);
        Date start = (Date)java.util.Collections.min(times);
        ArrayList<Date> newTimes = new ArrayList<Date>();

        if(start.getTime()%this.aggSpan!=0){
            //set start timestamp as the first timestep before
            start = new Date(start.getTime()-start.getTime()%this.aggSpan);
        }

        Date tmpDate = start;
        newTimes.add(start);
        while(tmpDate.before(end)){
            tmpDate = new Date(tmpDate.getTime()+this.aggSpan);
            newTimes.add(tmpDate);
        }
        
        return newTimes;
    }

    private double calculateAggregate(SortedMap<Date,Double> aggPoolMap){
        if(aggPoolMap.size()<1){
            throw new IllegalArgumentException("Cannot calculate aggregate" +
                    " on pool of zero values.");
        }

        double[] aggPool = new double[aggPoolMap.size()];
        int i=0;
        for(double d:aggPoolMap.values()){
            aggPool[i]=d;
            i++;
        }


        switch(selectedAggType){
            case MEAN:
                return StatUtils.mean(aggPool);
            case SUM:
                return StatUtils.sum(aggPool);
            case MAX:
                return StatUtils.max(aggPool);
            case MIN:
                return StatUtils.min(aggPool);
            case STDDEV:
                return StatUtils.variance(aggPool);
            default:
                throw new RuntimeException("Unknown aggregation type defined.");
        }
    }


    /**
     * Aggregation method to use when aggregating the data. Examples are mean,
     * standard deviation, max, etc.
     * @param t Aggregation method to use
     */
    public void setAggType(aggTypes t){
        this.selectedAggType = t;
    }

    public aggTypes getAggType(){
        return this.selectedAggType;
    }

    public void setAggSpan(long s){
        this.aggSpan = s;
    }

    public long getAggSpan(){
        return this.aggSpan;
    }

}
